package weeny.stackedGraph.pre;

import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.swing.Box;
import javax.swing.JComboBox;
import javax.swing.border.TitledBorder;


import prefuse.Constants;
import prefuse.action.layout.Layout;
import prefuse.data.tuple.TupleSet;
import prefuse.render.PolygonRenderer;
import prefuse.visual.VisualItem;
import stacked.graph.ColorPicker;
import stacked.graph.Layer;
import stacked.graph.StreamLayout;
import weeny.pre.util.Pre;

/**
 * Layout Action that computes a stream graph, in which a series of data values
 * are consecutively stacked on top of each other. Basically, I just wrap the
 * algorithm in {@link StreamLayout#layout(Layer[])}.
 * 
 * @author weeny
 *
 */
public class StreamGraphLayout extends Layout implements Constants{
	
	private String m_sizes = null;
	private double m_threshold = 0;
	private Rectangle2D bounds;
	private ColorPicker colorPicker = null;
	private String m_field = null;
    private int m_orientation = ORIENT_LEFT_RIGHT;

    float m_minValue = Float.MAX_VALUE;
    float m_maxValue = Float.MIN_VALUE;

    /**
     * Set the threshold of the visibility of the stripes. If the maximum hight
     * of a stripe is smaller than the threshold, it is set invisible, although
     * it is still placed correctly.
     */
    public void setThreshold(double threshold){
    	m_threshold = threshold;
    }
    /**
     * Set the orientation of the time line. We assume the data in m_sizes has
     * time stamps and has already sorted by time
     */
    public void setOrientation(int orient) {
        if ( orient != ORIENT_TOP_BOTTOM && orient != ORIENT_BOTTOM_TOP &&
             orient != ORIENT_LEFT_RIGHT && orient != ORIENT_RIGHT_LEFT) {
            throw new IllegalArgumentException(
                    "Invalid orientation value: "+orient);
        }
        m_orientation = orient;
    }
    
    /**
     * @see #StreamGraphLayout(String, String, String, double)
     */
	public StreamGraphLayout(String group, String polyField, String sizes) {
		this(group, polyField, sizes, 1);
	}
	
	/**
	 * This layout will process every item in the group. It will take the float
	 * array [v,v,v,....] in the field sizes, which should not be null, run the 
	 * stream graph layout, and put the boundary values [x,y,x,y,x,y...], which 
	 * can be rendered by {@link PolygonRenderer}, into the field polyField. The
	 * stripes having maximum width smaller than threshold will be set invisible
	 * 
	 * Please notice that every stripe has four corners that should not be drawn
	 * as curves, so we triple the coordinate of every corner to make the turn
	 * very sharp.
	 */
	public StreamGraphLayout(String group, String polyField, String sizes,
			double threshold) {
		super(group);
		this.m_sizes = sizes;
		this.m_threshold = threshold;
		this.m_field = polyField;
	}
	
	/**
	 * Assign a {@link ColorPicker} to this layout to colorize the stripes. If
	 * it is set, then the resulting visual items will follow the color scheme
	 * of this color picker. By default, it is null, which means the colors do
	 * not change.
	 */
	public void setColorPicker(ColorPicker colorPicker){
		this.colorPicker = colorPicker;
	}
	@Override
	public void run(double frac) {
		TupleSet ts = m_vis.getGroup(getGroup());
		ArrayList<Layer> array = new ArrayList<Layer>();
		Iterator<VisualItem> iter = ts.tuples();
		Map<String, VisualItem> name2Items = new HashMap<String, VisualItem>();
		int id = 0;
		while (iter.hasNext()){
			VisualItem vi = iter.next();
			if (!vi.isVisible()) continue;
			float[] value = (float[]) vi.get(m_sizes);
			array.add(new Layer(""+id, value));	
			name2Items.put(""+id, vi);
			id++;
		}
		Layer[] layers = array.toArray(new Layer[array.size()]);
		StreamLayout sl = new StreamLayout();
		sl.layout(layers);
		if (colorPicker != null) {
			colorPicker.colorize(layers);
		}
		
        bounds = getLayoutBounds();
        
        int len = layers[0].yBottom.length;
        m_minValue = Float.MAX_VALUE;
        m_maxValue = Float.MIN_VALUE;
        for(Layer layer : layers){
        	for(int i = 0; i<len; i++){
        		m_minValue = Math.min(layer.yBottom[i], m_minValue);
        		m_maxValue = Math.max(layer.yBottom[i], m_maxValue);
        		m_minValue = Math.min(layer.yTop[i], m_minValue);
        		m_maxValue = Math.max(layer.yTop[i], m_maxValue);
        	}
        }
        
        boolean horizontal = m_orientation == ORIENT_LEFT_RIGHT;
        horizontal = horizontal || m_orientation == ORIENT_RIGHT_LEFT;
        
        float span = (float) (horizontal? bounds.getWidth():bounds.getHeight());
        float inc =  span / (len-1);
        
        ArrayList<Float> array1 = new ArrayList<Float>();
		for (Layer layer : layers) {
			double maxWidth = -1;
			float min = (float) (horizontal?bounds.getMinX():bounds.getMinY());
			array1.clear();
			for(int i = 0; i<layer.yBottom.length; i++){
				double width = Math.abs(layer.yBottom[i]-layer.yTop[i]);
				maxWidth = Math.max(maxWidth, width);
			}
			for(int i = 0; i<layer.yBottom.length; i++){
				if (i == 0 || i == layer.yBottom.length-1){
					//the point is the corner, we add two more duplicated coords
					array1.add(i*inc+ min);
					array1.add(valuePos(layer.yBottom[i], horizontal));
					array1.add(i*inc+ min);
					array1.add(valuePos(layer.yBottom[i], horizontal));
				}
				array1.add(i*inc+ min);
				array1.add(valuePos(layer.yBottom[i], horizontal));
			}
			for(int i = layer.yTop.length-1; i>=0; i--){
				if (i == 0 || i == layer.yTop.length-1){
					//the point is the corner, we add two more duplicated coords
					array1.add(i*inc+ min);
					array1.add(valuePos(layer.yTop[i], horizontal));
					array1.add(i*inc+ min);
					array1.add(valuePos(layer.yTop[i], horizontal));
				}
				array1.add(i*inc+ min);
				array1.add(valuePos(layer.yTop[i], horizontal));
			}
			float[] value = new float[array1.size()];
			for(int i = 0; i<value.length/2; i++){
				if(horizontal){
					value[i*2] = array1.get(i*2);
					if (m_orientation == ORIENT_RIGHT_LEFT) {
						value[i*2] = (float) (bounds.getCenterX()*2-value[i*2]);
					}
					value[i*2+1] = array1.get(i*2+1);
				}
				else{
					value[i*2] = array1.get(i*2+1);
					value[i*2+1] = array1.get(i*2);
					if (m_orientation == ORIENT_BOTTOM_TOP) {
						value[i*2+1] = (float) (bounds.getCenterY()*2-value[i*2+1]);
					}
				}
			}
			name2Items.get(layer.name).set(m_field, value);
			if (colorPicker != null) {
    			Pre.setBothCol(name2Items.get(layer.name), layer.rgb);
			}
			if (maxWidth < m_threshold) {
				name2Items.get(layer.name).setVisible(false);
			}
			else {
				name2Items.get(layer.name).setVisible(true);
			}
		}
	}
	public float[][] restore(VisualItem item){
		float[] array = (float[]) item.get(m_field);
		ArrayList<Float> list = new ArrayList<Float>();
		for(int i = 0; i<array.length/2; i+=2){
			if (list.isEmpty()) {
				list.add(array[i]);
				list.add(array[i+1]);
			}
			else{
				if (list.get(list.size()-2).equals(array[i]) && 
					list.get(list.size()-1).equals(array[i+1])){
					continue;
				}
				list.add(array[i]);
				list.add(array[i+1]);
			}
		}
		float[][] result = new float[2][];
		result[0] = new float[list.size()];
		for(int i = 0; i<list.size(); i++){
			result[0][i] = list.get(i);
		}
		
		list.clear();
		for(int i = array.length-2; i>=array.length/2; i-=2){
			if (list.isEmpty()) {
				list.add(array[i]);
				list.add(array[i+1]);
			}
			else{
				if (list.get(list.size()-2).equals(array[i]) && 
					list.get(list.size()-1).equals(array[i+1])){
					continue;
				}
				list.add(array[i]);
				list.add(array[i+1]);
			}
		}
		result[1] = new float[list.size()];
		for(int i = 0; i<list.size(); i++){
			result[1][i] = list.get(i);
		}
		if (result[0].length != result[1].length) {
			throw new IllegalStateException("Top bottom do not match");
		}
		return result;
	}
	private float valuePos(float val, boolean horizontal){
		if (horizontal){
			return (float) (frac(val)*bounds.getHeight()+bounds.getMinY());
		}
		else {
			return (float) (frac(val)*bounds.getWidth()+bounds.getMinX());
		}
	}
	private float frac(float value){
		return (value-m_minValue)/(m_maxValue-m_minValue);
	}
	/**
	 * Generate a component to control the layout orientation
	 */
	public Component oriComboBox(){
		JComboBox combo = new JComboBox();		
		combo.addItem("left to right");
		combo.addItem("right to left");
		combo.addItem("top to bottom");
		combo.addItem("bottom to top");
		combo.setSelectedIndex(m_orientation);
		combo.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				//the index happens to be the actual value of the orientation
				setOrientation(((JComboBox)arg0.getSource()).getSelectedIndex());
				run(0);
				m_vis.repaint();
			}
		});
		Box box = Box.createVerticalBox();
		box.add(combo);
		box.setBorder(new TitledBorder("Stream Orientation"));
		return box;
	}
}